package jsr292.java.lang.invoke;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class Access {
  public static boolean isVisible(/*maybenull*/Class<?> callingClass, Field field) {
      return isVisible(callingClass, field.getModifiers(), field.getDeclaringClass());   
  }
  
  public static boolean isVisible(/*maybenull*/Class<?> callingClass, Method method) {
      return isVisible(callingClass, method.getModifiers(), method.getDeclaringClass());
  }
  
  public static boolean isVisible(/*maybenull*/Class<?> callingClass, Constructor<?> constructor) {
      return isVisible(callingClass, constructor.getModifiers(), constructor.getDeclaringClass());
  }
  
  public static boolean isVisible(/*maybenull*/Class<?> callingClass, int memberModifiers, Class<?> memberDeclaringClass) {
      if (callingClass == memberDeclaringClass)
	  return true;
      
      if (Modifier.isPrivate(memberModifiers))
	  return false;
      
      if (isPublicVisible(memberModifiers, memberDeclaringClass))
	  return true;
      
      if (callingClass == null)  // public access
          return false;
      
      if (isInSamePackage(callingClass, memberDeclaringClass)) {
	  return true;
      }
      
      return Modifier.isProtected(memberModifiers) && memberDeclaringClass.isAssignableFrom(callingClass);
  }
  
  public static boolean isPublicVisible(int memberModifiers, Class<?> memberDeclaringClass) {
      return Modifier.isPublic(memberModifiers) && Modifier.isPublic(memberDeclaringClass.getModifiers());
  }
  
  public static boolean isPublicVisible(MethodType methodType) {
      if (!Modifier.isPublic(methodType.returnType.getModifiers()))
          return false;
      Class<?>[] parameterTypes = methodType.parameterTypes;
      int parameterCount = parameterTypes.length;
      for(int i=0; i<parameterCount; i++) {
          if (!Modifier.isPublic(parameterTypes[i].getModifiers()))
              return false;
      }
      return true;
  }
  
  /**
   * @param value should not be null
   */
  public static boolean isVisible(Class<?> callingClass, Object value) {
      Class<?> valueClass = value.getClass();
      if (Modifier.isPublic(valueClass.getModifiers()))
	  return true;
      return isInSamePackage(callingClass, valueClass);
  }
  
  public static boolean isInSamePackage(Class<?> callingClass, Class<?> declaringClass) {
      return callingClass.getClassLoader() == declaringClass.getClassLoader() &&
             getPackage(callingClass).equals(getPackage(declaringClass));
  }
  
  private static String getPackage(Class<?> clazz) {
      String name = clazz.getName();
      int index = name.lastIndexOf('.');
      if (index == -1)
	  return ""; // default package
      return name.substring(0, index - 1);
  }
  
  // try to find a public super type that is a sub type of a declared type
  // @param declaredType null means any type are ok
  // TODO enhancement
  //      if we find a type is not public but visible from the calling class
  //      and if the declaredType == null, it will work
  public static Class<?> findPublicVisibleSuperType(Class<?> type, /*maybenull*/Class<?> declaredType) {
      if (Modifier.isPublic(type.getModifiers()))
          return type;
      
      while(type != declaredType) {
          Class<?> superclass = type.getSuperclass();
          if (declaredType==null || declaredType.isAssignableFrom(superclass)) {
              if (Modifier.isPublic(superclass.getModifiers()))
                  return superclass;    
          }

          if (declaredType==null || declaredType.isInterface()) {
              Class<?>[] interfaces = type.getInterfaces();
              if (interfaces.length != 0) {
                  Class<?> interfaze = findPublicVisibleSuperInterface(interfaces, declaredType);
                  if ( interfaze != null)
                      return interfaze;
              }
          }
          
          type = superclass;
          if (type == Object.class)
              return (declaredType == null)? Object.class: null;
      }
      return null;
  }
  
  private static Class<?> findPublicVisibleSuperInterface(Class<?>[] interfaces, /*maybenull*/Class<?> declaredType) {
      for(Class<?> interfaze:interfaces) {
          if (declaredType==null || declaredType.isAssignableFrom(interfaze)) {
            if (Modifier.isPublic(interfaze.getModifiers()))
                return interfaze;
          }

          Class<?>[] superInterfaces = interfaze.getInterfaces();
          if (superInterfaces.length != 0) {
              Class<?> superInterfaze = findPublicVisibleSuperInterface(superInterfaces, declaredType);
              if (superInterfaze != null)
                  return superInterfaze;
          }
      }
      return null;
  }
}
